#!/usr/bin/perl
use strict;

#
# The Tungsten Sandbox
# (C) 2011, 2012 Giuseppe Maxia, Continuent, Inc
# Released under the New BSD License
#

# TODO
#
#  - Add prefetch service
# 

use strict;
use warnings;
use Data::Dumper;
use Getopt::Long;
use File::Basename;
use English '-no_match_vars';

our $VERSION = '2.0.13';
# our $DRY_RUN = 0;
our $VERBOSE = 0;

{

#
# General purpose routines.
#
package Utils;
use English '-no_match_vars';

sub get_file_lines 
{
    my ($fname, $options) = @_;
    open my $FH, '<', $fname
        or die "can't open $fname ($OS_ERROR)\n";
    my @lines =();
    while (my $line = <$FH>) 
    {
        if ( $options->{no_blanks} ) 
        {
            next if $line =~ /^\s*$/;
        }
        if ( $options->{no_comments} ) 
        {
            next if $line =~ /^\s*#/;
        }
        push @lines, $line;
    }
    close $FH;
    return (wantarray ? @lines : \@lines);
}

sub get_manual
{
    my $perldoc = which('perldoc');
    if ($perldoc)
    {
        exec "perldoc $PROGRAM_NAME";
    }
    else
    {
        die  "The 'perldoc' program was not found on this computer.\n"
            ."You need to install it if you want to see the manual\n";
    }
}


#
# Prints the help, using the data contained in the parsing options
#
sub get_help {
    my ($parse_options, $msg) = @_;
    if ($msg) {
        warn "[***] $msg\n\n";
    }

    my $layout = '[options] ';
    my $HELP_MSG = q{};
    for my $op (
                sort { $parse_options->{$a}{so} <=> $parse_options->{$b}{so} }
                grep { $parse_options->{$_}{parse}}  keys %{ $parse_options }
               )
    {
        my $param =  $parse_options->{$op}{parse};
        my $param_str = q{    };
        my ($short, $long ) = $param =~ / (?: (\w+) \| )? (\S+) /x;
        if ($short)
        {
            $param_str .= q{-} . $short . q{ };
        }
        $long =~ s/ = s \@? / = name/x;
        $long =~ s/ = i / = number/x;
        $param_str .= q{--} . $long;
        $param_str .= (q{ } x (40 - length($param_str)) );
        my $text_items = $parse_options->{$op}{help};
        my $item_no=0;
        for my $titem (@{$text_items})
        {
            $HELP_MSG .= $param_str . $titem ;
            if (++$item_no == @{$text_items})
            {
                if ($VERBOSE && $parse_options->{$op}{value}) 
                {
                    if (length($parse_options->{$op}{value}) > 40)
                    {
                        $HELP_MSG .= "\n" . q{ } x 40;
                    }
                    $HELP_MSG .=  " ($parse_options->{$op}{value})";
                }
            }
            $HELP_MSG .= "\n";
            $param_str = q{ } x 40;
        }
        if ($VERBOSE && $parse_options->{$op}{allowed}) 
        {
            $HELP_MSG .=  (q{ } x 40) . "  (Allowed: {@{[join '|', sort keys %{$parse_options->{$op}{allowed}}  ]}})\n"
        }
   }

   print get_credits(),
          "Syntax: $PROGRAM_NAME $layout \n",
          $HELP_MSG;
    exit( defined $msg );
}
 

#
# Lists development credits
#
sub get_credits 
{
    my $CREDITS =
          qq(    Tungsten Tools,  version $VERSION\n)
        . qq(    Tungsten Sandbox - Cluster builder\n )
        . qq(    (C) 2011,2012 Giuseppe Maxia, Continuent, Inc\n);
    return $CREDITS;
}

#
# Makes sure that the options passed at the command line 
# have the right format, and that the mandatory ones are filled.
#
sub validate_options
{
    my ($options, $parse_options) = @_;
    my @to_be_defined;
    my @not_allowed;
    #
    # Checks that required options are filled
    #
    for my $must ( grep {$parse_options->{$_}->{must_have}} keys %{$parse_options})
    {
        unless (defined $options->{$must})
        {
            my $required = 0;
            if (ref($parse_options->{$must}->{must_have}) && ref($parse_options->{$must}->{must_have}) eq 'ARRAY' )
            # 
            # Conditional requirement, with a list of tasks where it is required
            # Using information in the parsing options, this loop determines if 
            # some options must be filled or not.
            {
                for my $task (@{$parse_options->{$must}->{must_have}})
                {
                    if ($task eq $options->{topology})
                    {
                        $required = 1;
                    }
                }
            }
            elsif ($parse_options->{$must}->{must_have} eq '1')
            # unconditional requirement
            {
                $required=1;
            }
            elsif ($parse_options->{$must}->{must_have} eq $options->{operation})
            # required only for a given operation
            {
                $required = 1;
            }
            push @to_be_defined, $must if $required;
        }
    }

    #
    # Checks that options requiring given keywords are not using anything different
    #
    for my $option (keys %{$options} ) {
        if (exists $parse_options->{$option}{allowed} && $options->{$option})
        {
            unless (exists $parse_options->{$option}{allowed}{$options->{$option}})
            {
                push @not_allowed, "Not allowed value '$options->{$option}' for option '$option' - "
                . " (Choose among: { @{[keys %{$parse_options->{$option}{allowed}} ]} })\n";
            }
        }
    }
    #
    # Reports errors, if any
    #
    if (@to_be_defined)
    {
        for my $must (@to_be_defined)
        {
            print "Option '$must' must be defined\n"
        }
    }
    if (@not_allowed)
    {
        for my $na (@not_allowed) 
        {
            print $na;
        }
    }
    if (@not_allowed or @to_be_defined)
    {
        exit 1;
    }
}

#
# Custom implementation of the 'which' command.
# Returns the full path of the command being searched, or NULL on failure.
#
sub which
{
    my ($executable) = @_;
    for my $dir ( split /:/, $ENV{PATH} )
    {
        $dir =~ s{/$}{};
        if ( -x "$dir/$executable" )
        {
            return "$dir/$executable";
        }
    }
    return;
}

sub write_to ($$$)
{
    my ($fname, $lines, $mode) = @_;
    open my $FH, $mode, $fname
        or die "can't open $fname ($OS_ERROR)\n";
    
    if (ref($lines) && ref($lines) ne 'ARRAY')
    {
        die "'write_to' reuires either a scalar or an array reference\n";
    }
    elsif (! ref($lines))
    {
        $lines = [ $lines ];
    }
    for my $line (@$lines)
    {
        print $FH $line;
        unless ($line =~ /\n/)
        {
            print $FH "\n";
        }
    }
    close $FH;
}

sub add_header 
{
    my ($fname) = @_;
    my $date = scalar localtime;
    write_to $fname, "#!/bin/bash", '>';
    write_to $fname, "# Created by tungsten-sandbox $VERSION on $date", '>>';
}

1;
} # end package Utils

{

#
# Routines that install sandboxes
#
package Tsandbox;
use English '-no_match_vars';

# Service names used for multiple masters deployments.
# You can't create more service than the number 
# of items in this array. Although you probably
# don't want to hit that limit, especially in all-masters 
# topology.
our @service_names = qw(
    alpha   bravo   charlie delta    echo 
    foxtrot golf    hotel   india    juliet 
    kilo    lima    mike    november oscar 
    papa    quebec  romeo   sierra   tango 
    uniform victor  whisky  xray     yankee  
    zulu    zero    one     two     three    
    four    five    six     seven   eight
    nine
);

sub install_master_slave_replicator 
{
    my ($params, $extra, $options) = @_;
    my $master_thl_port='';
    if ($params->{master_thl_port}) 
    {
        $master_thl_port = "--master-thl-port=$params->{master_thl_port}";
    }
    $extra = '' unless defined $extra;
    if ($options->{topology} eq 'star')
    {
        $extra .= ' --property=replicator.service.comments=true ';
    }
    my $command = "./tools/tungsten-installer";
    my $args = 
      " --master-slave "
    . " --master-host=$params->{master_host}"
    . " --cluster-hosts=$params->{cluster_hosts}"
    . " --datasource-port=$params->{mysql_port}"
    . " --datasource-user=msandbox"
    . "  --datasource-password=msandbox"
    . " --home-directory=$params->{home_directory}"
    . " --datasource-log-directory=$params->{log_directory}"
    . " --datasource-mysql-conf=$params->{my_conf}"
    . " --service-name=$params->{service}"
    . " --thl-directory=$params->{home_directory}/tlogs"
    . " --thl-port=$params->{thl_port} $master_thl_port"
    . " --rmi-port=$params->{rmi_port}"
    . " $extra "
    . " --start"
    ;

    Tsandbox::run_user_var($options,'TR_BEFORE');
    #print pretty_command($command, $args) if $params->{verbose};
    log_installation( $options, pretty_command($command, $args));
    my $result = system "$command $args";
    if ($result) 
    {
        die "installation failed ($OS_ERROR)\n";
    }
    Tsandbox::run_user_var($options,'TR_AFTER');
}


sub install_direct_slave_replicator 
{
    my ($params, $extra, $options) = @_;
    my $master_thl_port='';
    if ($params->{master_thl_port}) 
    {
        $master_thl_port = "--master-thl-port=$params->{master_thl_port}";
    }
    $extra = '' unless defined $extra;
    my $command = "./tools/tungsten-installer";
    my $args = 
      " --direct "
    . " --master-host=$params->{master_host}"
    . " --master-port=$params->{master_port}"
    . " --slave-host=$params->{cluster_hosts}"
    . " --slave-port=$params->{mysql_port}"
    . " --master-user=msandbox"
    . " --master-password=msandbox"
    . " --slave-user=msandbox"
    . " --slave-password=msandbox"
    . " --home-directory=$params->{home_directory}"
    . " --master-log-directory=$params->{master_log_directory}"
    . " --slave-log-directory=$params->{log_directory}"
    . " --master-mysql-conf=$params->{master_my_conf}"
    . " --slave-mysql-conf=$params->{my_conf}"
    . " --service-name=$params->{service}"
    . " --thl-directory=$params->{home_directory}/tlogs"
    . " --thl-port=$params->{thl_port} $master_thl_port"
    . " --rmi-port=$params->{rmi_port}"
    . " $extra "
    . " --start"
    ;

    #print pretty_command($command, $args) if $params->{verbose};
    log_installation($options,  pretty_command($command, $args));
    my $result = system "$command $args";
    if ($result) 
    {
        die "installation failed ($OS_ERROR)\n";
    }
#
#TUNGSTEN_BASE=$HOME/tinstall/tsb/
#./tools/tungsten-installer \
#    --direct \
#    --master-host=127.0.0.1 \
#    --master-port=7101 \
#    --slave-host=db2 \
#    --slave-port=7102 \
#    --master-user=root \
#    --slave-user=root \
#    --master-password=msandbox \
#    --slave-password=msandbox \
#    --master-log-directory=$HOME/sandboxes/tr_dbs/node1/data \
#    --service-name=Castor \
#    --thl-port=12112 \
#    --channels=5 \
#    --rmi-port=20000 \
#    --home-directory=$TUNGSTEN_BASE \
#
#./tools/tungsten-installer \
#    --direct \
#    --master-host=127.0.0.1 \
#    --master-port=7101 \
#    --slave-host=db3 \
#    --slave-port=7103 \
#    --master-user=root \
#    --slave-user=root \
#    --master-password=msandbox \
#    --slave-password=msandbox \
#    --master-log-directory=$HOME/sandboxes/tr_dbs/node1/data \
#    --service-name=Pollux \
#    --thl-port=22112 \
#    --channels=1 \
#    --rmi-port=20000 \
#    --home-directory=$TUNGSTEN_BASE \
#    --relay-directory=$TUNGSTEN_BASE/relay --start-and-report

}

sub install_direct 
{
    my ($options, $sandbox_dir) = @_;

    my %rmi_ports;
    my $env_node_options    = $ENV{TR_NODE_OPTIONS}   || '';
    my $env_master_options  = '';
    my $env_slave_options   = $ENV{TR_SLAVE_OPTIONS}  || '';

    #
    # There is no master to install
    #
    my %slave_params = (
        master_host          => '127.0.0.1',
        cluster_hosts        => '127.0.0.1',
        master_port          => $options->{base_port}   +1,
        mysql_port           => $options->{base_port}   +1,
        thl_port             => $options->{thl_port},
        rmi_port             => $options->{rmi_port},
        home_directory       => "$options->{tungsten_base}/$options->{tsb_prefix}1",
        master_log_directory => "$sandbox_dir/node1/data",
        master_my_conf       => "$sandbox_dir/node1/my.sandbox.cnf",
        service              => "$options->{service}",
        verbose              => $options->{verbose},
    );
    mkdir "$options->{tungsten_base}/$options->{tsb_prefix}1";

    my $starting_direct_slave = 2 ;
    if ($options->{mysql_slaves})
    {
        for my $N (2 .. 1 + $options->{mysql_slaves})
        {
            my $change_master_to = sprintf(qq{CHANGE MASTER TO master_host='%s',master_port=%d, master_user='%s',master_password='%s'},
                '127.0.0.1',
                $slave_params{master_port},
                'msandbox',
                'msandbox'
            );
            $slave_params{mysql_port}           += 1 ;
            mkdir "$options->{tungsten_base}/$options->{tsb_prefix}$N";
            log_installation($options, qq($sandbox_dir/n$N -e "$change_master_to; start slave"));
            system qq($sandbox_dir/n$N -e "$change_master_to; start slave");
        }
        $starting_direct_slave = 2 + $options->{mysql_slaves}; 
    }

    for my $N ($starting_direct_slave .. $options->{nodes} )
    {
        $slave_params{mysql_port}           += 1 ;
        $slave_params{thl_port}             += 1 ;
        $slave_params{rmi_port}             += 2 ;
        $slave_params{master_extra_options} = '';
        $slave_params{home_directory}       = "$options->{tungsten_base}/$options->{tsb_prefix}$N";
        $slave_params{log_directory}        = "$sandbox_dir/node$N/data";
        $slave_params{my_conf}              = "$sandbox_dir/node$N/my.sandbox.cnf";

        $rmi_ports{$N} = $slave_params{rmi_port};
        mkdir $slave_params{home_directory};
        Tsandbox::install_direct_slave_replicator(\%slave_params, $env_node_options . ' ' . $env_slave_options , $options); 
    }
    return \%rmi_ports;
}

sub check_tree_options
{
    my ($options) = @_;
    return unless $options->{tree};
    if ($options->{tree} =~ /\s*(\d+)\s*:\s*(\d+)/)
    {
        my ($intermediate, $final) = ($1, $2);    
        if (    ($intermediate < 1) 
             or ($intermediate > $options->{nodes})
             or ($final < 1) 
             or ($final > $options->{nodes})
           )
           
        {
            die "tree nodes out of range\n";   
        }
        elsif ($intermediate == $final)
        {
            die "intermediate and final node must be different\n";
        }
        else
        {
            if ($options->{topology} ne 'master-slave')
            {
                die "the --tree option can only be used with master-slave topology\n";
            }
            return {intermediate=> $intermediate, final =>$final};
        }
    }
    else
    {
        get_help('Invalid tree value');
    }
}

sub install_master_slave 
{
    my ($options, $sandbox_dir) = @_;

    my %rmi_ports;
    my $env_node_options    = $ENV{TR_NODE_OPTIONS}   || '';
    my $env_master_options  = $ENV{TR_MASTER_OPTIONS} || '';
    my $env_slave_options   = $ENV{TR_SLAVE_OPTIONS}  || '';

    mkdir "$options->{tungsten_base}/$options->{tsb_prefix}1";

    #
    # installs the master
    #
    my %master_params = (
        master_host          => '127.0.0.1',
        cluster_hosts        => '127.0.0.1',
        mysql_port           => $options->{base_port}   +1,
        thl_port             => $options->{thl_port},
        rmi_port             => $options->{rmi_port},
        home_directory       => "$options->{tungsten_base}/$options->{tsb_prefix}1",
        log_directory        => "$sandbox_dir/node1/data",
        my_conf              => "$sandbox_dir/node1/my.sandbox.cnf",
        service              => "$options->{service}",
        master_thl_port      => undef,
        verbose              => $options->{verbose},
    );

    $rmi_ports{1} = $options->{rmi_port};

    mkdir $master_params{home_directory};
    Tsandbox::install_master_slave_replicator(\%master_params, $env_node_options . ' ' . $env_master_options, $options );

    my %slave_params = %master_params;

    for my $N (2 .. $options->{nodes} )
    {
        $slave_params{master_thl_port}      = $master_params{thl_port};
        $slave_params{mysql_port}           += 1 ;
        $slave_params{thl_port}             += 1 ;
        $slave_params{rmi_port}             += 2 ;
        $slave_params{master_extra_options} = '';
        $slave_params{home_directory}       = "$options->{tungsten_base}/$options->{tsb_prefix}$N";
        $slave_params{log_directory}        = "$sandbox_dir/node$N/data";
        $slave_params{my_conf}              = "$sandbox_dir/node$N/my.sandbox.cnf";

        $rmi_ports{$N} = $slave_params{rmi_port};
        mkdir $slave_params{home_directory};
        Tsandbox::install_master_slave_replicator(\%slave_params, $env_node_options . ' ' . $env_slave_options, $options ); 
        if ($options->{tree_actions})
        {
            if ($N == $options->{tree_actions}{intermediate})
            {
                $options->{tree_actions}{thl_port} = $slave_params{thl_port};
            }
        }
    }
    return \%rmi_ports;
}

sub set_tree_topology
{
    my ($options) = @_;
    my $trepctl = "$options->{tungsten_base}/$options->{tsb_prefix}$options->{tree_actions}{final}/trepctl";
    unless ( -x $trepctl)
    {
        die "something went wrong with the tree topology setup\n";
    }
    system "$trepctl offline";
    system "$trepctl setrole -role slave -uri thl://127.0.0.1:$options->{tree_actions}{thl_port}";
    system "$trepctl online";
}

sub install_all_masters 
{
    my ($options, $sandbox_dir) = @_;

    my %rmi_ports;

    my $env_node_options    = $ENV{TR_NODE_OPTIONS}   || '';
    my $env_master_options  = $ENV{TR_MASTER_OPTIONS} || '';
    if ($options->{nodes} > @service_names)
    {
        die "Can't install so many services. The maximum allowed is " . scalar(@service_names) . " \n";
    }
    my %master_params = (
        master_host        => '127.0.0.1',
        cluster_hosts      => '127.0.0.1',
        mysql_port         => $options->{base_port},
        thl_port           => $options->{thl_port} -1,
        rmi_port           => $options->{rmi_port} -2,
        master_thl_port    => undef,
        verbose            => $options->{verbose},
    );

    my %node_params = %master_params;
    for my $N (1 .. $options->{nodes} )
    {
        mkdir "$options->{tungsten_base}/$options->{tsb_prefix}$N";

        $node_params{service}       = $service_names[$N -1] ;
        $node_params{mysql_port}   += 1 ;
        $node_params{thl_port}     += 1 ;
        $node_params{rmi_port}     += 2 ;
        $node_params{home_directory} = "$options->{tungsten_base}/$options->{tsb_prefix}$N";
        $node_params{log_directory}  = "$sandbox_dir/node$N/data";
        $node_params{my_conf}        = "$sandbox_dir/node$N/my.sandbox.cnf";

        $rmi_ports{$N} = $node_params{rmi_port};

        mkdir $node_params{home_directory};
        Tsandbox::install_master_slave_replicator(\%node_params, $env_master_options . ' ' . $env_node_options, $options );
    }

    return \%rmi_ports;
}

#
# Runs the contents of an environment variable.
# If the variable is not set it doesn't do anything.
# If the variable is set, it runs its value without any additional checks.
#
sub run_user_var
{
    my ($options, $var) = @_;
    return unless $ENV{$var};
    if ($ENV{$var})
    {
        if ($options->{verbose})
        {
            print "Running contents of \$$var\n";
            print "$ENV{$var}\n";
        }
        my $result = system $ENV{$var};
        if ($result)
        {
            die "error running \$$var. ($!)\n";
        }
    }
     

}

sub install_service ($$$)
{
    my ($options, $command, $node) =@_;
    my $configure_service = "$options->{tungsten_base}/$options->{tsb_prefix}$node/tungsten/tools/configure-service";
    unless (-x $configure_service)
    {
        die "Could not find executable for configure-service at $configure_service\n";
    }
    if ($ENV{TRS_NODE_OPTIONS})
    {
        $command .= ' ' . $ENV{TRS_NODE_OPTIONS};
    }
    #if ($options->{verbose})
    #{
    #    print pretty_command($configure_service, $command);
    #}
    log_installation($options,  pretty_command($configure_service, $command));
    Tsandbox::run_user_var ($options,'TRS_BEFORE');
    my $result = system "$configure_service $command";
    if ($result) 
    {
        die "installation failed ($OS_ERROR)\n";
    }
    Tsandbox::run_user_var ($options,'TRS_AFTER');
}

#
# Logs the given command to a file in the sandbox directory
#
sub log_installation
{
    my ($options, $command) = @_;
    if ($options->{verbose})
    {
        print "$command\n";
    }
    unless ( $options->{tungsten_base} && ( -d $options->{tungsten_base}))
    {
        die "can't find tungsten base directory\n";
    }
    my $fname = "$options->{tungsten_base}/tungsten_installation.log";
    open my $FH, '>>', $fname
        or die "can't open $fname ($OS_ERROR)\n";
    print $FH "#", scalar localtime, "\n";
    print $FH $command, "\n";
    close $FH;
}

#
# Returns a nicely formatted set of options for a command with a long list of arguments
#
sub pretty_command
{
    my ($cmd, $args) = @_;
    my $indent = ' ' x 4;
    $args =~ s{\s+-}{ \\\n$indent-}g;
    return "$cmd$args\n";
}

#
# Install slave services for all nodes 
#
sub install_all_slave_services
{
    my ($options) = @_;

    my $params =   ' -C --quiet --host=127.0.0.1 '
                     . ' --datasource=127_0_0_1 '
                     . ' --role=slave '
                     . ' --service-type=remote --master-thl-host=127.0.0.1 --svc-start '
                     . '--local-service-name=%s --master-thl-port=%d %s';
    for my $master (1 .. $options->{nodes} )
    {
        for my $slave ( 1 .. $options->{nodes})
        {
            if ($master != $slave)
            {
                my $command = sprintf($params, 
                        $service_names[$master -1], 
                        $options->{thl_port} + $slave -1, 
                        $service_names[$slave -1]) ;
                Tsandbox::install_service($options, $command, $master);
            }
        }
    }
}

#
# Installs all services needed for a star schema
#

sub install_star_services
{
    my ($options) = @_;

    my $endpoint_params =  ' -C --quiet --host=127.0.0.1 --datasource=127_0_0_1 --role=slave '
                       . ' --service-type=remote --master-thl-host=127.0.0.1 --svc-start '
                       . ' -a --svc-allow-any-remote-service=true '
                       . ' --skip-validation-check=THLStorageCheck '
                       . '--local-service-name=%s --master-thl-port=%d %s';
    
    my $hub_params  =  ' -C --quiet --host=127.0.0.1 --datasource=127_0_0_1 --role=slave '
                       . ' --service-type=remote --master-thl-host=127.0.0.1 --svc-start '
                       . ' --skip-validation-check=THLStorageCheck '
                       . ' --log-slave-updates=true '
                       . '--local-service-name=%s --master-thl-port=%d %s';

    for my $master (1 .. $options->{nodes} )
    {
        next if $master == $options->{hub};

        my $command = sprintf($endpoint_params, 
              $service_names[$master -1], 
              $options->{thl_port} + $options->{hub} -1, 
              $service_names[$options->{hub} -1]) ;
        $command .= $ENV{TRS_ENDPOINT_OPTIONS} || '';
        Tsandbox::install_service($options, $command, $master);
        $command = sprintf( $hub_params, 
              $service_names[$options->{hub} -1], 
              $options->{thl_port} + $master -1, 
              $service_names[$master -1]) ;
        $command .= $ENV{TRS_HUB_OPTIONS} || '';
        Tsandbox::install_service($options, $command, $options->{hub});
     }
}

#
# Installs slave service in a fan-in node
#
sub install_fan_in_services
{
    my ($options) = @_;

    my $fan_in_params  =  ' -C --quiet --host=127.0.0.1 --datasource=127_0_0_1 --role=slave '
                        . ' --service-type=remote --master-thl-host=127.0.0.1 --svc-start '
                        . ' --skip-validation-check=THLStorageCheck '
                        . '--local-service-name=%s --master-thl-port=%d %s';

    for my $master (1 .. $options->{nodes} )
    {
        next if $master == $options->{fan_in};

        my $command = sprintf($fan_in_params, 
              $service_names[$options->{fan_in} -1], 
              $options->{thl_port} + $master -1, 
              $service_names[$master -1]) ;
        Tsandbox::install_service($options, $command, $options->{fan_in});
     }
}

#
# Creates utility and admin scripts
#
sub create_sandbox_scripts
{
    my ($options, $rmi_ports, $sandbox_dir) = @_;

    my $services_filter = Utils::which('simple_services') || '';
    if ($services_filter)
    {
        $services_filter = " | $services_filter";
    }

    # 
    # Creates utility scripts in each indivisual sandbox
    #
    for my $N ( 1 .. $options->{nodes})
    {
        chdir $options->{tungsten_base};
        if ( -e "n$N") 
        {
            unlink "n$N";
        }
        system "ln -s $sandbox_dir/n$N n$N";
        chdir "$options->{tsb_prefix}$N";
        #
        # Creates a symlink to the utility scripts of the corresponding MySQL Sandbox 
        #
        system "ln -s $sandbox_dir/node$N/use db_use";
        system "ln -s $sandbox_dir/node$N/start db_start";
        system "ln -s $sandbox_dir/node$N/status db_status";
        system "ln -s $sandbox_dir/node$N/restart db_restart";
        system "ln -s $sandbox_dir/node$N/stop db_stop";
        #
        # Creates utility scripts to deal with the replicator in this sandbox
        #
        Utils::add_header('show_log');   # shows the logs
        Utils::add_header('show_conf');  # show the properties files
        Utils::add_header('replicator'); # runs the replicator
        Utils::add_header('trepctl');    # runs trepctl
        Utils::add_header('services');   # runs trepctl services
        Utils::add_header('thl');        # runs thl
        if (($options->{topology} eq 'direct') && $N <= (1 + $options->{mysql_slaves}))
        {
            for my $s( qw(show_conf show_log replicator trepctl services thl))
            {
                Utils::write_to($s, "# 'Command $s not available in direct mode'", '>>')
            }
        }
        else
        {
            my $curdir = "$options->{tungsten_base}/$options->{tsb_prefix}$N";
            Utils::write_to('show_log', "vim -o $curdir/tungsten/tungsten-replicator/log/*.log", '>>');
            Utils::write_to('show_conf', "vim -o $curdir/tungsten/tungsten-replicator/conf/static-*.properties", '>>');
            Utils::write_to('replicator', "$curdir/tungsten/tungsten-replicator/bin/replicator " . '$@', '>>');
            Utils::write_to('trepctl', "$curdir/tungsten/tungsten-replicator/bin/trepctl -port $rmi_ports->{$N} " . '$@', '>>');
            Utils::write_to('services', "$curdir/tungsten/tungsten-replicator/bin/trepctl -port $rmi_ports->{$N} services $services_filter" , '>>');
            Utils::write_to('thl', "$curdir/tungsten/tungsten-replicator/bin/thl " . '$@', '>>');
        }
        chmod 0750, qw(show_conf show_log replicator trepctl services thl);
    }

    #
    # Creates symlinks to the sandbox group utilities
    #
    chdir $options->{tungsten_base};
    for my $script (qw(use start stop clear restart status send_kill))
    {
        if ( -e "${script}_all")
        {
            unlink "${script}_all";
        }
        system "ln -s $sandbox_dir/${script}_all db_${script}_all";
    }

    #
    # Create utilities that clean up or remove a Tungsten Sandbox
    #
    Utils::add_header('clear_tsandbox');
    Utils::add_header('erase_tsandbox');
    Utils::write_to('clear_tsandbox', "$options->{tungsten_base}/replicator_all stop", '>>');
    Utils::write_to('clear_tsandbox', "$options->{tungsten_base}/db_clear_all stop", '>>');
    for my $N ( 1 .. $options->{nodes})
    {
        Utils::write_to('clear_tsandbox', '# ', '>>');
        if (($options->{topology} ne 'direct') or ($N > (1 + $options->{mysql_slaves}) ))
        {
            Utils::write_to('clear_tsandbox', 
                [
                "find $options->{tungsten_base}/$options->{tsb_prefix}$N/tlogs/ -type f -exec rm {} \\;", 
                "rm -rf $options->{tungsten_base}/$options->{tsb_prefix}$N/tungsten/tungsten-replicator/log/*", 
                ],
                '>>');
        }
    }
    chmod 0750, 'clear_tsandbox';
    Utils::write_to('erase_tsandbox', 
        [
        "$options->{tungsten_base}/clear_tsandbox", 
        "rm -rf $options->{tungsten_base}/*", 
        ],
        '>>');
    chmod 0750, 'erase_tsandbox';

    Utils::add_header('restart_tsandbox');
    Utils::write_to('restart_tsandbox', 
        [
        "$options->{tungsten_base}/db_start_all", 
        "$options->{tungsten_base}/replicator_all start", 
        "$options->{tungsten_base}/trepctl_all services", 
        ],
        '>>');
    chmod 0750, 'restart_tsandbox';

    #
    # Creates scripts that can invoke all replicators or trepctls with a single command.
    #
    for my $script (qw(trepctl replicator services))
    {
        Utils::add_header("${script}_all");
        for my $N (1 .. $options->{nodes})
        {
            Utils::write_to("${script}_all", 
                [ 
                "echo '#$N'",
                "$options->{tungsten_base}/$options->{tsb_prefix}$N/$script \$\@", 
                ],
                '>>');
        }
        chmod 0750, "${script}_all";
    }

    #
    # Writes information about how the sandbox was created.
    #
    $options->{sandbox_directory} = $sandbox_dir;
    Utils::write_to('tungsten_sandbox.info', 'Tungsten Sandbox created at ' . scalar(localtime), '>');
    Utils::write_to('tungsten_sandbox.info', Utils::get_credits(), '>');
    
    for my $opt ( grep {$options->{$_}} ('nodes','mysql_version','installation_directory', 
                   'tungsten_release', 'sandbox_directory', 'topology', 
                   'fan-in','hub', 'base_port', 'thl_port','rmi_port', 'tree'))
    {
        Utils::write_to('tungsten_sandbox.info', sprintf('%-30s => %s',$opt, $options->{$opt} || '') ,'>>');
    }
    for my $opt (qw(masters slaves))
    {
        Utils::write_to('tungsten_sandbox.info', sprintf('%-30s => %s', $opt, '[' . join( ',',@{$options->{$opt}}) . ']' ), '>>' );
    }

    #
    # Creates a test script that will probe the data flow.
    #
    my @tables =();
    Utils::add_header('test_topology');
    my $extra = '';
    if ($options->{topology} eq 'star') 
    {
        $extra = ' (hub: ' . $options->{hub} . ')'; 
    }
    if ($options->{tree_actions})
    {
        $extra = '( tree: ' . $options->{tree} . ')'; 
    }
    Utils::write_to('test_topology', "echo '# Testing topology '$options->{topology}' $extra with $options->{nodes} nodes.'", '>>');
    Utils::write_to('test_topology', 
                "echo '# Master nodes: [@{$options->{masters}}] - Slave nodes: [@{ $options->{slaves}}]'", 
                '>>');
    for my $N (@{$options->{masters}})
    {
        Utils::write_to('test_topology',
            [ 
            "$options->{tungsten_base}/n$N -N -B -e 'drop table if exists test.t$N'",
            "$options->{tungsten_base}/n$N -N -B -e 'drop table if exists test.v$N'",
            "$options->{tungsten_base}/n$N -N -B -e 'create table test.t$N(i int not null primary key, c char(20)) engine= innodb'",
            "$options->{tungsten_base}/n$N -N -B -e 'create or replace view test.v$N as select * from  test.t$N'",
            qq[$options->{tungsten_base}/n$N -N -B -e 'insert into test.v$N VALUES ($N, "inserted by node #$N")'],
            ],
            '>>');
        push @tables, $N;
    }
    
    Utils::write_to('test_topology', "sleep 2" , '>>');
    my %trepctl_seen;
    my $num_masters = scalar @{$options->{masters}};
    for my $N (@{$options->{slaves}})
    {
        Utils::write_to('test_topology', "echo '# node $N'" , '>>');
        write_test('test_topology',
            [
            qq[COUNT_ALL=\$($options->{tungsten_base}/n$N -N -B -e 'select count(*) from information_schema.tables where table_schema="test" and table_type="BASE TABLE"')],
            qq(if [ "\$COUNT_ALL" == "$num_masters" ] ; then echo -n 'ok' ; else echo -n 'not ok' ; fi ; echo ' - Tables from all masters' ),
            ]
         );

        write_test('test_topology',
            [
            qq[COUNT_ALL=\$($options->{tungsten_base}/n$N -N -B -e 'select count(*) from information_schema.tables where table_schema="test" and table_type="VIEW"')],
            qq(if [ "\$COUNT_ALL" == "$num_masters" ] ; then echo -n 'ok' ; else echo -n 'not ok' ; fi ; echo ' - Views from all masters' ),
            ]
        );

        for my $M (@{$options->{masters}})
        {
            write_test( 'test_topology',
                [
                qq[COUNT=\$($options->{tungsten_base}/n$N -N -B -e 'select count(*) from test.t$M where c = "inserted by node #$M" ')],
                qq(if [ "\$COUNT" == "1" ] ; then echo -n 'ok' ; else echo -n 'not ok' ; fi ; echo ' - Records from master #$M' ),
                ]
            );
        }
        write_test('test_topology',
            [
            "SERVICES=\$($options->{tungsten_base}/$options->{tsb_prefix}$N/trepctl services | grep 'serviceName' | awk '{print \$3}')",
            'for SERVICE in $SERVICES ; do',
            "    STATE=\$($options->{tungsten_base}/$options->{tsb_prefix}$N/trepctl -service \$SERVICE status | grep 'state' | awk '{print \$3}')",
            qq(    if [ "\$STATE" == "ONLINE" ] ; then echo -n 'ok' ; else echo -n 'not ok' ; fi ; echo " - Node #$N-\$SERVICE online" ),
            'done',
            ]
        );
    }
    Utils::write_to('test_topology',
        ' echo "1..$TESTS"',
        '>>');
    chmod 0750, 'test_topology';
}

sub write_test 
{
    my ($fname, $lines) = @_;
    for my $line (@$lines)
    {
        Utils::write_to($fname, $line, '>>');
        if ($line =~ /\bok\b/)
        {
            my $indent = '';
            if ($line =~ /^(\s+)/)
            {
                $indent = $1;
            }
            Utils::write_to($fname, $indent . 'TESTS=$(($TESTS+1))' ,'>>');
        }
    }
}


1;
} # end package Tsandbox

#
# Definition of the options for this application.
#
# Each option has the following fields
# * parse       ->  what will be parsed at the command line
# * value       ->  the default value of this option
# * so          ->  the sort order. Used by the get_help routine to list the options
# * must_have   ->  either a number (inconditional requirement) 
#                   or a keyword list indicating for which topology it is required
# * help        ->  the text to be displayed at the help
# Optionally, a list of 'allowed' keywords can be provided. If so, the application will accept
# only values that match the given list
#
my %parse_options = (

    nodes => {
            parse   => 'n|nodes=i',
            value   => 3,
            so      => 10,
            help    => ['How many nodes to install' ]
        },
    'mysql_version' => {
            parse   => 'm|mysql-version=s',
            value   => undef,
            must_have => 1,
            so      => 20,
            help    => ['which MySQL version to use' ]
        },
     tungsten_base => {
            parse   => 't|tungsten-base=s',
            value   => "$ENV{HOME}/tsb2",
            so      => 30,
            help    => ['Where to install the sandbox' ]
        },
     installation_directory => {
            parse   => 'i|installation-directory=s',
            value   => $ENV{PWD},
            so      => 35,
            help    => ['Where the Tungsten tarball has been expanded' ]
        },
     group_dir => {
            parse   => 'd|group-dir=s',
            value   => 'tr_dbs',
            so      => 40,
            help    => ['sandbox group directory name' ]
        },
     skip_sandbox_creation => {
            parse   => 'skip-sandbox-creation',
            value   => 0,
            so      => 45,
            help    => ['Skips sandbox creation (using an existing one)' ]
        },

     topology => {
            parse   => 'topology=s',
            value   => 'master-slave',
            so      => 50,
            allowed => {'master-slave' => 1, 'direct'=> 1, 'bi-dir' => 1, 'all-masters' => 1, 'fan-in' => 1, 'star' => 1},
            help    => ['Which topology to deploy' ]
        },

     hub => {
            parse   => 'hub=i',
            value   => undef,
            so      => 60,
            must_have => ['star'],
            help    => ['Which node is a hub' ]
        },
   

     fan_in => {
            parse   => 'fan-in=i',
            value   => undef,
            so      => 65,
            must_have => ['fan-in'],
            help    => ['Which node is the fan-in slave' ]
        },

     tree => {
            parse   => 'tree=s',
            value   => undef,
            so      => 66,
            help    => [ 'Define hierarchical replication. ',
                         'You must indicate which node does not replicate directly from the master',
                         'The format is --tree=X:Y, where X is the intermediate node,',
                         'and Y is the ultimate one. (e.g. --tree=2:4 means that node 4 replicates from',
                         'the master through node 2)' ]
        },


     mysql_slaves => {
            parse   => 'mysql-slaves=i',
            value   => 0,
            so      => 70,
            must_have => ['direct'],
            help    => ['How many nodes in the cluster will be MySQL native slaves' ]
        },

     tsb_prefix => {
            parse   => 'x|tsb-prefix=s',
            value   => 'db',
            so      => 80,
            help    => ['Tungsten Sandbox prefix ',
                        '(each directory inside ~/tsb2/ containing a complete tungsten ',
                        'sandbox will be named using this prefix and the node number)' ]
        },
     service => {
            parse   => 's|service=s',
            value   => 'tsandbox',
            so      => 85,
            help    => ['How the service is named' ]
        },
     service_names => {
            parse   => 'service-names=s',
            value   => join(',',@Tsandbox::service_names),
            so      => 86,
            help    => ['How the services in multiple master topologies are named' ]
        },
 
     base_port => {
            parse   => 'p|base-port=i',
            value   => 7100,
            so      => 90,
            help    => ['Base port for MySQL Sandbox nodes ' ]
        },
     thl_port => {
            parse   => 'l|thl-port=i',
            value   => 12110,
            so      => 100,
            help    => ['Port for the THL service ' ]
        },
     rmi_port => {
            parse   => 'r|rmi-port=i',
            value   => 10100,
            so      => 110,
            help    => ['Port for the RMI service ' ]
        },
     version => {
            parse   => 'v|version',
            value   => 0,
            so      => 120,
            help    => ['Show Tungsten sandbox version and exit ' ]
        },
     show_options => {
            parse   => 'show-options',
            value   => 0,
            so      => 125,
            help    => ['Show Tungsten sandbox collected options and exit ' ]
        },
     ignore_manifest => {
            parse   => 'ignore-manifest',
            value   => 0,
            so      => 126,
            help    => [
                        'Ignore build values in .manifest when determining if the Tungsten package',
                        'can be used with this sandbox' 
                       ]
        },
     verbose => {
            parse   => 'verbose',
            value   => 0,
            so      => 130,
            help    => ['Show more information during installation and help ' ]
        },
     manual => {
            parse   => 'man|manual',
            value   => 0,
            so      => 140,
            help    => ['display the program manual' ]
        },
     install_options => {
            parse   => 'o|install-options=s@',
            value   => undef,
            so      => 150,
            help    => [
                            'Options to be passed to the underlying installer, with the format' ,
                            '{TR|MSB}:ROLE:options',
                            'Where TR is Tungsten Replicator, MSB is MySQL Sandbox, and ROLE can be',
                            'one of BEFORE AFTER MASTER, SLAVE, DIRECT, NODE, HUB, ENDPOINT, GLOBAL'
                       ]
        },
      help => {
            parse   => 'h|help',
            value   => 0,
            so      => 300,
            help    => ['display this help' ]
        },
);

#
#  Generates the working options set from the parsing options.
#
my %options = map { $_ ,  $parse_options{$_}{'value'}}  keys %parse_options;

#
# Parses the options, using the 'parse' element of each parse_option item
# Calls the help if there is a parsing error.

GetOptions (
    map { $parse_options{$_}{parse}, \$options{$_} }    
        grep { $parse_options{$_}{parse}}  keys %parse_options 
) or Utils::get_help(\%parse_options, '');

if (@ARGV)
{
    Utils::get_help(\%parse_options, "Unexpected arguments: <@ARGV>");
}

if ($options{verbose}) 
{
    $VERBOSE = $options{verbose};
}

if ($options{version})
{
    print Utils::get_credits();
    exit 0;
}
Utils::get_help(\%parse_options)  if $options{help};
Utils::get_manual()  if $options{manual};

#
#  Makes sure that the options conform to the expectations
#
Utils::validate_options(
    \%options, 
    \%parse_options);

#if  (($options{topology} eq 'star') or ($options{topology} eq 'all-masters'))
#{
#    if ($options{mysql_version} ge '5.5')
#    {
#        die "The '$options{topology}' topology is not safe to run with MySQL 5.5. This restriction will be lifted in further versions\n";
#    }
#}

if ($options{service_names})
{
    my @service_list = split /[;, ]/, $options{service_names};
    my $max_services = scalar @Tsandbox::service_names;
    if (scalar @service_list > $max_services )
    {
        die "too many services named. We can only use as many as $max_services \n" ;
    }

    for my $N ( 0 .. $max_services -1 )
    {
        $Tsandbox::service_names[$N] = $service_list[$N];
    }
}

if ($options{service_names})
{
    my @service_list = split /[;, ]/, $options{service_names};
    my $max_services = scalar @Tsandbox::service_names;
    if (scalar @service_list > $max_services )
    {
        die "too many services named. We can only use as many as $max_services \n" ;
    }

    for my $N ( 0 .. $max_services -1 )
    {
        $Tsandbox::service_names[$N] = $service_list[$N];
    }
}

unless ($parse_options{topology}{allowed}{$options{topology}})
{
    die "Topology $options{topology} has not been implemented yet\n";
}


my $make_multiple_sandbox = Utils::which('make_multiple_sandbox');
unless ($make_multiple_sandbox or $options{skip_sandbox_creation}) 
{
    die "Can't find make_multiple_sandbox in PATH ($ENV{PATH}).\n"
        ."Perhaps MySQL Sandbox is not installed.\n"
        ."Get it from CPAN or from http://mysqlsandbox.net\n";
}

my @applications_options_list= qw( TR TRS MSB );
my @role_options_list = qw( BEFORE AFTER MASTER SLAVE DIRECT NODE HUB ENDPOINT GLOBAL ); 
my %role_actions = (BEFORE=>1, AFTER=>1);

my %customer_install_options= (
);
for my $app (@applications_options_list)
{
    for my $role (@role_options_list)
    {
        if ($role_actions{$role})
        {
            $customer_install_options{$app}{$role} =  "${app}_${role}";
        }
        else
        {
            $customer_install_options{$app}{$role} =  "${app}_${role}_OPTIONS";
        }
        my $env_variable = $customer_install_options{$app}{$role};
        $ENV{$env_variable} = '' unless $ENV{$env_variable};
    }
}

#
# Checks the options that Tungsten Sandbox may pass to MySQL Sandbox and Tungsten installer. 
#

if ($options{install_options} )
{
    unless (ref $options{install_options} && ref($options{install_options}) eq 'ARRAY')
    {
        $options{install_options} = [ $options{install_options} ];
    }
    for my $option ( @{ $options{install_options} } )
    {
        my ($app, $role, $value);
        if ($option =~ /^(\w+):(\w+):(.+)/)
        {
            ($app, $role, $value) = ($1, $2, $3);
            unless (exists $customer_install_options{$app}{$role})
            {
                die "invalid APP:ROLE combination '$app:$role'\n";
            }
        }
        else
        {
            die   "Invalid option '$option'\n"
                . "A valid sequence is made of APP:ROLE:option\n"
                . "Where APP is one of (@applications_options_list) \n"
                . "and ROLE is one of (@role_options_list)\n";
        }
        $ENV{$customer_install_options{$app}{$role}} .= $value;
        if ($options{verbose})
        {
            print "\$ENV{$customer_install_options{$app}{$role}} = $ENV{$customer_install_options{$app}{$role}}\n";
        }
    }
}

# print Dumper \%options; exit;

unless ($options{skip_sandbox_creation})
{
    my $minimum_mysql_sandbox_version = '3.0.24';
    my $mysql_sandbox_help = qx($make_multiple_sandbox --help);
    if ($mysql_sandbox_help =~ /version\s+(\d+\.\d+\.\d+)/)
    {
        my $sb_version = $1;
        if ($sb_version lt $minimum_mysql_sandbox_version)
        {
            die "This application requires MySQL Sandbox version $minimum_mysql_sandbox_version. Your version is $sb_version. Please upgrade\n";
        }
    }
    else
    {
    die "unable to find MySQL Sandbox version using $make_multiple_sandbox\n";
    }
}

#
# Find installation directory.
#

if ($options{'installation_directory'})
{
    $options{'installation_directory'} =~ s[^\./][$ENV{PWD}/];
    chdir $options{'installation_directory'};
}

if ( -f ".manifest" ) 
{
    my @manifest_lines = Utils::get_file_lines('.manifest');
    my $build_no =0;
    my $release ='';
    my $minimum_release='2.0.6';
    my $minimum_build=627;
    for my $line (@manifest_lines) 
    {
        if ($line =~ /RELEASE:\s*(\S+)/) 
        {
            $options{tungsten_release} = $1;
            $release = $1;
            $release =~ s/-\d+$//;
        }
        if ($line =~ /BUILD_NUMBER:\s*(\d+)/) 
        {
            $build_no=$1;
            $options{tungsten_build} = $build_no;
            last;
        }
    }
    if (
        ( ($release le $minimum_release)  && ($build_no < $minimum_build) )
        && 
        ( ! $options{'ignore_manifest'}))
    {
        die "Builds earlier than $minimum_build can't be used with this version of tungsten-sandbox\n"
            ."Your build is $build_no - Please download a more recent version\n"
            ."If you have built the software yourself, you may use the --ignore-manifest option to continue\n";
    }
} 
else 
{
    die   "Could not find .manifest\n"
        . "Please expand the Tungsten Replicator tarball and cd inside the inner directory\n";
}

#
# Checking that the MySQL version exists
#
my $SANDBOX_HOME= $ENV{SANDBOX_HOME} || "$ENV{HOME}/sandboxes";
my $SANDBOX_BINARY= $ENV{SANDBOX_BINARY} || "$ENV{HOME}/opt/mysql";

if ( -d "$SANDBOX_BINARY/$options{mysql_version}" )
{
    $ENV{PATH} .= ":$SANDBOX_BINARY/$options{mysql_version}/bin"
}
else
{
    die "directory $SANDBOX_BINARY/$options{mysql_version} not found\n";
}

unless ( -d $options{tungsten_base} )
{
    die "directory $options{tungsten_base} not found\n";
}

$ENV{HOME} =~ s{/$}{};
$options{tungsten_base} =~ s{/$}{};

if ( $options{tungsten_base} eq $ENV{HOME}) 
{
    die "directory \$HOME cannot be \$TUNGSTEN_BASE\n";
}

unless ( -x './tools/tungsten-installer') 
{
    die   "Can't find an executable './tools/tungsten-installer' \n"
        . "Please run this command from inside an expanded Tungsten Replicator tarball\n";
}

if ($options{show_options})
{
    print Utils::get_credits();
    print "Options: \n";
    for my $opt ( sort { $parse_options{$a}{so} <=> $parse_options{$b}{so} }  keys %parse_options)
    {
        printf "%-25s : %s\n", $opt, (defined $options{$opt}? $options{$opt} : 'NULL');
    }
    exit 0;
}

if ($options{nodes} < 2)
{
    die "There must be at least 2 nodes \n";
}

if ($options{'hub'} and ($options{topology} ne 'star'))
{
    die "The option 'hub' can only be used with the 'star' topology\n";
}

if ($options{'fan_in'} and ($options{topology} ne 'fan-in'))
{
    die "The option 'fan-in' can only be used with the 'fan-in' topology\n";
}

if ($options{mysql_slaves} and ($options{topology} ne 'direct'))
{
    die "The option 'mysql_slaves' can only be used with the 'direct' topology\n";
}

if (($options{nodes} - $options{mysql_slaves}) < 2) 
{
    die  "Not enough nodes for the requested topology:\n"
        ."If you want $options{mysql_slaves} mysql slaves, then you need at least " . ($options{mysql_slaves} + 2) . " nodes \n";
}

if ($options{hub})
{
    die "The value for --hub must be an integer\n" unless $options{hub} =~/^\d+$/;
    if (($options{hub} > $options{nodes}) or ($options{hub} < 1))
    {
        die "The value for '--hub' must an integer between 1 and the number of nodes ($options{nodes})\n";
    }
}

if ($options{topology} eq 'bi-dir')
{
    if ($options{nodes} > 3) # user has requested more nodes
    {
        warn "Bi-dir topology is done with two nodes only. Use the 'all-masters' topology for more nodes. \n";
    }
    # whith this change, bi-dir will be installed as all-masters
    $options{nodes}=2;
}

$options{tree_actions} = Tsandbox::check_tree_options(\%options);

#
# ------------------ POINT OF NON RETURN ---------------------
#
# After this line, modifications start to happen in the system
#
# ------------------------------------------------------------

##print Dumper \%parse_options; 
# print Dumper \%options; exit;
my $sandbox_dir = "$SANDBOX_HOME/$options{group_dir}";

if ($options{skip_sandbox_creation})
{
    unless ( ( -d $sandbox_dir) && ( -x "$sandbox_dir/n1" ) && ( -x "$sandbox_dir/start_all") && ( -x "$sandbox_dir/status_all")  ) 
    {
        die "sandbox $sandbox_dir does not seem to contain an usable sandbox\n";
    }
    my @sandbox_dirs = grep { -d $_ } glob("$sandbox_dir/*/");
    my $sandbox_nodes = scalar grep { m{\/node\d+/$} } @sandbox_dirs;
    #print "[@sandbox_dirs] $sandbox_nodes\n"; exit;
    if ($sandbox_nodes < $options{nodes})
    {
        die "The sandbox in $sandbox_dir doesn't have enough available nodes (found: $sandbox_nodes)\n";
    }    
    my @using = Utils::get_file_lines("$sandbox_dir/node1/USING", {no_blanks=>1});
    my $sb_version;
    for my $line (@using)
    {
        if ($line =~ /basedir.+(\d\.\d\.\d+)/)
        {
            $sb_version = $1;
        }
    }
    unless ($sb_version)
    {
        die "could not find version in $sandbox_dir/node1/USING\n";
    }
    if ($sb_version ne $options{mysql_version})
    {
        die "incompatible sandbox version. Requested $options{mysql_version}, but found $sb_version\n";
    }

    my $sandbox_status = qx($sandbox_dir/status_all);
    if ($sandbox_status =~ /\boff\b/)
    {
        system "$sandbox_dir/start_all";
    }
    $sandbox_status = qx($sandbox_dir/status_all);
    if ($sandbox_status =~ /\boff\b/)
    {
        die "The sandbox in $sandbox_dir does not start\n";
    }

}
else
{
    my $additional_flags='';
    if ($options{mysql_version} =~ /^5\.6/)
    { 
        $additional_flags='-c binlog_checksum=none -c log_bin_use_v1_row_events';
    }
    my $mms_cmd =   "$make_multiple_sandbox "
              . " --node_options='$ENV{MSB_NODE_OPTIONS} $additional_flags -c log-bin=mysql-bin -c innodb_flush_log_at_trx_commit=1 -c max_allowed_packet=48M'"
              . " --how_many_nodes=$options{nodes}"
              . " --sandbox_base_port=$options{base_port}"
              . " --group_directory=$options{group_dir}"
              . " $options{mysql_version}"
              ;
    Tsandbox::run_user_var(\%options,'MSB_BEFORE');
    system $mms_cmd;

    unless ( ( -d $sandbox_dir) && ( -x "$sandbox_dir/n1" ) && ( -x "$sandbox_dir/start_all") ) 
    {
        die "sandbox installation failed\n";
    }
    Tsandbox::run_user_var(\%options,'MSB_AFTER');
}

for my $N ( 1 .. $options{nodes} )
{
    system qq($sandbox_dir/n$N -u root -e 'update mysql.user set Grant_priv="Y" where user="msandbox"; flush privileges');
}

system "rm -rf $options{tungsten_base}/$options{tsb_prefix}*" ;
system "rm -rf $options{tungsten_base}/n?" ;
system "rm -rf $options{tungsten_base}/db_{clear,use,send_kill,status,start,stop}_all" ;
system "rm -rf $options{tungsten_base}/{replicator,trepctl,clear,use,send_kill,status,start,stop}_all" ;

my $rmi_ports;

#
# Main task  
#


if  ($options{topology} eq 'master-slave')
{
    $rmi_ports = Tsandbox::install_master_slave(\%options, $sandbox_dir);
    $options{masters} = [1];
    $options{slaves} = [grep {$_ != 1} ( 1 .. $options{nodes})];
}
elsif  ($options{topology} eq 'direct')
{
    $rmi_ports = Tsandbox::install_direct(\%options, $sandbox_dir);
    $options{masters} = [1];
    $options{slaves} = [grep {$_ != 1} ( 1 .. $options{nodes})];
}
elsif  (($options{topology} eq 'all-masters') or  ($options{topology} eq 'bi-dir'))
{
    $rmi_ports = Tsandbox::install_all_masters(\%options, $sandbox_dir);
    Tsandbox::install_all_slave_services(\%options);
    $options{masters} = [(1 .. $options{nodes})];
    $options{slaves} = [(1 .. $options{nodes})];
}
elsif  ($options{topology} eq 'star')
{
    unless ($options{hub})
    {
        die "The 'star' topology requires a hub number\n";
    }
    $rmi_ports = Tsandbox::install_all_masters(\%options, $sandbox_dir);
    Tsandbox::install_star_services(\%options);
    $options{masters} = [(1 .. $options{nodes})];
    $options{slaves} = [(1 .. $options{nodes})];
}
elsif  ($options{topology} eq 'fan-in')
{
    unless ($options{'fan_in'})
    {
        die "The 'fan-in' topology requires a fan-in number \n";
    }
    $rmi_ports = Tsandbox::install_all_masters(\%options, $sandbox_dir);
    Tsandbox::install_fan_in_services(\%options);
    $options{masters} = [ grep {$_ != $options{fan_in}} (1 .. $options{nodes})];
    $options{slaves} = [$options{fan_in}];
}
else
{
    die "topology $options{topology} not implemented yet\n";
}

Tsandbox::create_sandbox_scripts(\%options, $rmi_ports, $sandbox_dir);
if ($options{tree_actions})
{
    Tsandbox::set_tree_topology(\%options);
}


__END__

=head1 NAME

Tungsten Sandbox - Quickly installs Tungsten Replicator clusters with MySQL

=head1 SYNOPSIS


 tungsten-sandbox --mysql_version=5.1.60 \
    --installation-directory=tungsten-replicator-2.0.5/  

 tungsten-sandbox --mysql_version=5.1.60 \
    --installation-directory=tungsten-replicator-2.0.5/ \
    --topology=all-masters --nodes=5

    Tungsten Tools,  version 2.0.08
    Tungsten Sandbox - Cluster builder
     (C) 2011 Giuseppe Maxia, Continuent, Inc
 Syntax: tungsten-sandbox-2.0/tungsten-sandbox [options] 

    -n --nodes = number                 How many nodes to install (3)
    -m --mysql-version = name           which MySQL version to use
    -t --tungsten-base = name           Where to install the sandbox (/Users/gmax/tsb2)
    -i --installation-directory = name  Where the Tungsten tarball has been expanded
                                         (/Users/gmax/workdir/continuent/tungsten-toolbox/tungsten-sandbox)
    -d --group-dir = name               sandbox group directory name (tr_dbs)
    --skip-sandbox-creation             Skips sandbox creation (using an existing one)
    --topology = name                   Which topology to deploy (master-slave)
                                          (Allowed: {all-masters|bi-dir|direct|fan-in|master-slave|star})
    --hub = number                      Which node is a hub
    --fan-in = number                   Which node is the fan-in slave
    --mysql-slaves = number             How many nodes in the cluster will be MySQL native slaves
    -x --tsb-prefix = name              Tungsten Sandbox prefix (db)
    -s --service = name                 How the service is named (tsandbox)
    -p --base-port = number             Base port for MySQL Sandbox nodes  (7100)
    -l --thl-port = number              Port for the THL service  (12110)
    -r --rmi-port = number              Port for the RMI service  (10100)
    -v --version                        Show Tungsten sandbox version and exit 
    --show-options                      Show Tungsten sandbox collected options and exit 
    --ignore-manifest                   Ignore build values in .manifest when determining if the Tungsten package
                                        can be used with this sandbox
    --verbose                           Show more information during installation and help 
    -man --manual                       display the program manual
    -o --install-options = name         Options to be passed to the underlying installer, with the format
                                        {TR|MSB}:ROLE:options
                                        Where TR is Tungsten Replicator, MSB is MySQL Sandbox, and ROLE can be
                                        one of BEFORE AFTER MASTER, SLAVE, DIRECT, NODE, HUB, ENDPOINT, GLOBAL
    -h --help                           display this help

=head1 PURPOSE

Tungsten Sandbox is a tool that installs a testing environment for database replication, using B<MySQL::Sandbox> and B<Tungsten Replicator>. Rather than using virtual machines or separate servers, this tool can deploy database servers and replication software, in simple and complex topologies, using a single host.
Using this tool you will be able to deploy:

=over 3

=item *
B<Master/ Slave> is the vanilla replication between one master and N slaves.

=item *
B<direct> is a hybrid replication topology where one or more Tungsten slaves attach directly to a MySQL master without installing a master service.
=item *
B<All-Masters> is a replication cluster where every node is a master and each node is a slave of all the other nodes. This is done by installing, for each node, a master service and one slave service for each of the other nodes.

=item *
B<Bi-dir> is a special case of all-masters, when the cluster has only two nodes.

=item *
B<Star> is a cluster where there is a hub wich is connected in bi-directional replication with several endpoints, each of which is a master. Compared to the all-masters topology, it requires less connections.

=item *
B<Fan-in> is a cluster where a single slave receives updates from many masters at the same time.

=back

=head1 REQUIREMENTS

It should work with Tungsten Replicator 2.0.4-167 or later and MySQL Sandbox 3.0.24 or later. All the requirements for a normal installation of Tungsten should be met. See the online documentation for details. But notice that the installer will tell you if the requirements are met for most of them.

=head1 INSTALLATION

As of version 2.0, tungsten-sandbox is self contained in one file, which you can use anywhere, provided that you can indicate where is the expanded tarball to be used.

=head1 Creating sandboxes

To create a sandbox, you need to provide at least two pieces of information: where are the MySQL binaries and where you have expanded the Tungsten Replicator tarball. The first piece is given as a version number, and this means that a MySQL binary tarball has been expanded into the default folder for C<$SANDBOX_BINARY>, i.e C<$HOME/opt/mysql> and renamed to the version number. Please refer to the MySQL Sandbox documentation for more information:
 perldoc MySQL::Sandbox
 perldoc MySQL::Sandbox::Recipes
The second piece of information is simply the directory where you have expanded the Tungsten Replicator tarball:

either
  tungsten-sandbox --mysql-version=5.1.60 \
     --installation-directory=/tmp/tungsten-replicator-2.0.5-452/

or
  tungsten-sandbox -m 5.1.60 -i /tmp/tungsten-replicator-2.0.5-452/

If your current working directory is inside that tarball, you don't need to specify the installation directory option

  cd /tmp/tungsten-replicator-2.0.5-452/
  tungsten-sandbox -m 5.1.60

=head2 Reusing sandboxes

If you want to use an existing sandbox instead of creating a new one, you cand do it by specifying C<--skip-sandbox-creation>. In this case, Tungsten Sandbox will look for an existing sandbox named with the value of C<--group-dir> under C<$SANDBOX_HOME>. If the requested version matches, and the sandbox is a multiple sandbox as needed by Tungsten-Sandbox, the existing sandbox will be used. If not, an appropriate error will be shown.

=head2 Defaults

To avoid typing too much, there are defaults defined for most everything that can be reasonably expected. Here's what you get:

 nodes                  : 3
 mysql-version          : NULL
 tungsten-base          : $HOME/tsb2
 installation-directory : NULL
 group-dir              : tr_dbs
 topology               : master-slave
 hub                    : NULL
 service                : tsandbox
 tsb-prefix             : db
 base-port              : 7100
 thl-port               : 12110
 rmi-port               : 10100
 verbose                : 0

Some explanation is needed: 

C<tungsten-base> is where the sandbox will be installed. 

C<group-dir> is how the multiple MySQL sandbox will be named (it will contain N single sandboxes)

C<tsb-prefix> is how the single Tungsten sandboxes will be called inside C<tungsten-base>.

=head2 Master/slave

If you don't define explicitly any topology, the default is C<master-slave>, and since the default number of nodes is 3, you will get one master and two slaves.
Thus, indicating a MySQL version and Tungsten installation directory will give you a plain master/slave topology, with a service named C<tsandbox>.

  tungsten-sandbox -m 5.1.60 \
     -i /tmp/tungsten-replicator-2.0.5-452/

=head2 Direct

The main feature of this topology is that there is no master replicator service. The slave service is attached directly to the MySQL master, and it can also occasionally take over an existing MySQL native slave.
The lack of master service makes some differences in the way the sandboxes are organized. For sandbox #1, there is no replicator, trepctl, thl. The corresponding commands are replaced by a reminder taht there is no such command.

=head2 Mixed

This topology is a "direct" deployment, with the addition of native MySQL replication slaves. If you use the option C<--mysql-slaves=1> with a direct topology, then you will have 1 MySQL vanilla slave, and one Tungsten direct slave.
You can add as many MySQL slaves as wanted, provided that the number is within the nodes you want to define. For example 

  tungsten-sandbox -m 5.1.60 \
     --topology=direct --nodes=5 --mysql-slaves=2

This command will create two MySQL slaves (nodes #2 and #3) and two Tungsten direct slaves (nodes #4 and #5). 

=head2 All-masters

This topology installs one master for each node, and then, for each node, it defines a slave service for each of the remaining nodes. This means that each node is directly replicating to all the other nodes. The number of services will soon become difficult to maintain.
There must be a service name for each node. Tungsten-sandbox uses the international spelling alphabet for this purpose (alpha, bravo, charlie ... plust the ten digits) and this is why you can't have more than 36 nodes, which is probably too much to set in a single host already, but feel free to try.

  tungsten-sandbox -m 5.1.60 \
    -i /tmp/tungsten-replicator-2.0.5-452/  \
    --nodes=4 --topology=all-masters

=head2 Bi-Dir

This is an all-master topology with only two nodes. The following two commands are equivalent

  tungsten-sandbox -m 5.1.60 \
    -i /tmp/tungsten-replicator-2.0.5-452/  \
    --topology=bi-dir

  tungsten-sandbox -m 5.1.60 \
    -i /tmp/tungsten-replicator-2.0.5-452/  \
    --nodes=2 --topology=all-masters

=head2 Star

For this topology, you are required to provide a 'hub' node number. 
Given a cluster with 4 nodes where node 3 is the hub, there will be a master-master relationship between 3 and 1, 3 and 2, 3 and 4. This way, changes entered in any node will be propagated to the rest of the cluster, but with a lower number of services than the all-masters topology. 

  tungsten-sandbox -m 5.1.60 \
    -i /tmp/tungsten-replicator-2.0.5-452/  \
    --nodes=4 --topology=star --hub=3


=head2 Fan-in

In this topology, you define a slave node (using the 'hub' option), and all the other nodes will be masters of that node.

  tungsten-sandbox -m 5.1.60 \
    -i /tmp/tungsten-replicator-2.0.5-452/  \
    --nodes=4 --topology=fan-in --hub=3

In this example, nodes 1,2, and 4 will be masters, all of them replicating to node 3.

=head1 Fine tuning installation

Tungsten-Sandbox offers some hooks to modify the installation, should you need it. 
There is a B<--install_options> parameter that you can use to pass installation options in several phases of the sandbox build.
The syntax is 

  --install-options=APP:ROLE:parameters

Where B<APP> is any of TR (= Tungtsen Replicator installer), TRS (= service installer with tools/configure-service), or MSB (= MySQl Sandbox installer).

B<ROLE> is where you want to do the change: MASTER, SLAVE, HUB, ENDPOINT, DIRECT.

So, for example, if you want to add parallel replication to the slaves, you can do this

  tungsten-sandbox -m 5.1.60 \
  --install-options='TR:SLAVE:--channels=5'

Likewise, to add parallel replication only on endpoint services of a star schema topology, you may do this:

  tungsten-sandbox -m 5.1.60 \
     --schema=star --hub=1 \
     --install-options='TRS:ENDPOINT:--channels=5 --buffer-size=100'

=head1 Using a sandbox

Creating a sandbox is not even half the task. You need to use it to get some benefits. Tungsten-sandbox provides customized tools that let you use a sandbox easily. First, an overview of what you will find in $TUNGSTEN_BASE

  $TUNGSTEN_BASE
    |          global scripts to use MySQL databases
    |          global scripts to use Tungsten replicators
    | db1
    |    scripts to use the replicator in this sandbox
    |  
    | db2
    |    scripts to use the replicator in this sandbox
    |
    | db3
    |    scripts to use the replicator in this sandbox
    
It is worth repeating: Tungsten sandboxes are installed in the directory defined by C<--tungsten_base>, which by default isC<$HOME/tsb2>.

=head2 All at once

In the tungsten base directory, there are several convenience scripts

=over 3

=item *
B<clear_tsandbox>
An useful and dangerous command. It stops the replicators and shuts down the databases, erasing everything, making the sandbox ready for clean use.

=item * 
B<restart_tsandbox>   
Starts all the sandboxes at once. It is useful after using clear_tsandbox.

=item *
B<erase_tsandbox>
An even more dangerous command than clear_tsandbox. In addition to clearing everything, this command will remove everything under $HOME/tsb2.

=item *
B<replicator_all>   
Runs a given command to all the replicators in the underlying sandboxes.

  $HOME/tsb2/replicator_all status
  $HOME/tsb2/replicator_all stop
  $HOME/tsb2/replicator_all start
  $HOME/tsb2/replicator_all status

=item *
B<trepctl_all>
Runs a given C<trepctl> command in all the sandboxes
  
  $HOME/tsb2/trepctl_all services
  $HOME/tsb2/trepctl_all status

=item *
B<services_all>
Runs the C<services> command in all the sandboxes. See the 'services' command below for more information.
  
  $HOME/tsb2/services_all
  #1
  tsandbox  [master]
  seqno:       0  - latency:   0.394 - ONLINE

  #2
  tsandbox  [slave]
  seqno:       0  - latency:   4.506 - ONLINE

  #3
  tsandbox  [slave]
  seqno:       0  - latency:  13.285 - ONLINE

=item * 
B<db_restart_all>, B<db_start_all>, B<db_stop_all>, B<db_clear_all>, B<db_send_kill_all>, B<db_status_all>, B<db_use_all> send the corresponding commands to MySQL Sandbox.

=item * 
B<n1>, B<n2>, B<n3> (and more if more nodes were defined) will use the database at the corresponding node. It is equivalent to run

  mysql -h 127.0.0.1 -u msandbox -pmsandbox -P 7101  # = n1
  mysql -h 127.0.0.1 -u msandbox -pmsandbox -P 7102  # = n2
  mysql -h 127.0.0.1 -u msandbox -pmsandbox -P 7103  # = n3

=item * 
B<db1/>, B<db2/>, B<db3/> are the directory where each Tungsten replicator is installed. See L<Each sandbox> below.
Notice that the name may change, depending on the value of C<--tsb_prefix>.

=item *
B<test_topology> is a customized script that tests the topology by creating tables and records in all the masters defined in the topology, and retrieving such objects from all the slaves. In this context, "master" and "slave" mean master and slave services. Thus if a node includes both, it will be used for generating data and for retrieving it.

=back

=head2 Each sandbox

In each sandbox inside C<$TUNGSTEn_BASE>, there are several convenience scripts that mirror the ones seen for the base, but act in the limited scope of their sandbox.

=over 3

=item * 
B<db_restart>, B<db_status>, B<db_start>, B<db_stop>, B<db_use> will send the corresponding command to the database associated with this replicator. So, inside db1, the db_use command will give access to $HOME/sandboxes/tr_dbs/node1/use.

=item * 
B<trepctl> is the main tool to inspect the replicator. This script give access to the proper path, with the port defined for the replicator

  $HOME/tsb2/db1/trepctl status
  # corresponds to:
  $HOME/tsb2/db1/tungsten/tungsten-replicator/bin/trepctl -port 10100 status

=item * 
B<services> runs "trepctl services" for this replicator. It may seem a little improvement to have a dedicated script that saves 8 keystrokes. That too, but the main improvement is that this command invokes the 'simple_services' script, it it exists. 
'simple services' is a filter script that displays 'services' information in a compact way.

 $HOME/tsb2/db1/services
  tsandbox  [master]
  seqno:       0  - latency:   0.394 - ONLINE

Compare to the unfiltered output:

 $HOME/tsb2/db1/trepctl services
  Processing services command...
  NAME              VALUE
  ----              -----
  appliedLastSeqno: 0
  appliedLatency  : 0.394
  role            : master
  serviceName     : tsandbox
  serviceType     : local
  started         : true
  state           : ONLINE
  Finished services command...

This script (in combination with simple_services), is particularly useful for topologies that deploy many services.

=item * 
B<thl>: gives access to the thl utility

  $HOME/tsb2/db1/thl index

=item * 
B<replicator> is the Tungsten Replicator itself

  $HOME/tsb2/db1/replicator status

=item * 
B<show_log> is a shortcut to read the replicator logs. 

=item * 
B<show_conf> is a shortcut to access the replicator properties files

=item *
B<tungsten/> This directory is where the replicator binaries are installed. It is a symbolic link to $HOME/tsb2/db1/releases/tungsten-replicator-x.x.x-xxx

=item *
B<releases/> is the place where the tungsten binatries are installed.

=item *
B<configs/> is where the configuration files created by the installer are stored.

=item *

B<tlogs/> is where the THL files are kept. The default name in regular instalations is 'thl', but it was changed for Tungsten sandbox, because we have a 'thl' script on the same directory.

=back

=head1 COPYRIGHT

Version 2.0

Copyright (C) 2011 Giuseppe Maxia, Continuent, Inc

Released under the New BSD license.

Home Page  L<http://code.google.com/p/tungsten-toolbox>

